package gobelinmaker.data;

import gobelinmaker.server.LineIntersection;
import ij.IJ;
import ij.ImagePlus;
import ij.gui.Roi;
import ij.plugin.filter.MaximumFinder;
import java.awt.Polygon;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import org.apache.commons.math3.stat.regression.SimpleRegression;

/**
 * Lyukakat felismerő osztály.
 *
 * @author imruf84
 */
public class HoleFinder {

    /**
     * Maximum kereső.
     */
    private final MaximumFinder maxFinder = new MaximumFinder();
    /**
     * Képfeldolgozó.
     */
    private final ImagePlus imgp = new ImagePlus();
    /**
     * Pontok tűréshatára (vízszintestől/függőlegestől való eltérés).
     */
    private final int ep = 7;
    /**
     * Egyenesek meredekségének tűréshatára.
     */
    private final double er = 0.2;

    /**
     * Lyukak felismerése képen.
     *
     * @param image kép
     * @return megtalált lyukak
     */
    public HoleFinderResult findHoles(BufferedImage image) {

        // Kép tárolása.
        imgp.setImage(image);

        // Feldolgozás.
        //imgp.getProcessor().invert();
        imgp.getProcessor().medianFilter();
        //imgp.getProcessor().blurGaussian(3);
        IJ.run(imgp, "Subtract Background...", "rolling=50 light separate sliding disable");
        IJ.run(imgp, "Make Binary", "");
        IJ.run(imgp, "Fill Holes", "");
        IJ.run(imgp, "Erode", "");
        IJ.run(imgp, "Ultimate Points", "");
        imgp.getProcessor().multiply(255);

        // Vágás.
        double clipSize = Math.min(image.getWidth(), image.getHeight()) * 0.9;
        Rectangle2D clip = new Rectangle2D.Double((image.getWidth() - clipSize) / 2, (image.getHeight() - clipSize) / 2, clipSize, clipSize);
        imgp.getProcessor().fillOutside(new Roi(clip.getX(), clip.getY(), clip.getWidth(), clip.getHeight()));

        Polygon p = maxFinder.getMaxima(imgp.getProcessor(), 0, true);
        Point2D points[] = new Point2D[p.npoints];
        for (int i = 0; i < p.npoints; i++) {
            points[i] = new Point2D.Double(p.xpoints[i], p.ypoints[i]);
        }

        // Sorok.
        LinkedList<LinkedList<Point2D>> rows = new LinkedList<>();
        // Oszlopok.
        LinkedList<LinkedList<Point2D>> cols = new LinkedList<>();

        // Végig haladunk a pontokon.
        for (Point2D point : points) {

            // Alapesetben nem felel meg a pont a sornak.
            boolean isPointRowOk = false;
            // Végig nézzük a sorokat.
            for (LinkedList<Point2D> row : rows) {

                // Alapesetben megfelel a sor.
                boolean isRowOk = true;

                // A sorok pontjai.
                for (Point2D rPoint : row) {
                    // Ha a tűréshatáron kívül van, akkor nem fogadjuk el.
                    if (ep < Math.abs(rPoint.getY() - point.getY())) {
                        isRowOk = false;
                        break;
                    }
                }

                // Ha megfelelt a sornak akkor tároljuk.
                if (isRowOk) {
                    row.add(point);
                    isPointRowOk = true;
                    break;
                }

            }

            // Ha nem felelt meg a pont, akkor új sorként tároljuk.
            if (!isPointRowOk) {
                LinkedList<Point2D> row = new LinkedList<>();
                rows.add(row);
                row.add(point);
            }

            // Alapesetben nem felel meg a pont az oszlopnak.
            boolean isPointColOk = false;
            // Végig nézzük az oszlopokat.
            for (LinkedList<Point2D> col : cols) {

                // Alapesetben megfelel az oszlop.
                boolean isColOk = true;

                // Az oszlopok pontjai.
                for (Point2D cPoint : col) {
                    // Ha a tűréshatáron kívül van, akkor nem fogadjuk el.
                    if (ep < Math.abs(cPoint.getX() - point.getX())) {
                        isColOk = false;
                        break;
                    }
                }

                // Ha megfelelt az oszlopnak akkor tároljuk.
                if (isColOk) {
                    col.add(point);
                    isPointColOk = true;
                    break;
                }

            }

            // Ha nem felelt meg a pont, akkor új oszlopként tároljuk.
            if (!isPointColOk) {
                LinkedList<Point2D> col = new LinkedList<>();
                cols.add(col);
                col.add(point);
            }
        }

        // Sorokra illeszkedő egyenesek előállítása.
        LinkedList<Line2D> rowLines = new LinkedList<>();
        for (LinkedList<Point2D> row : rows) {
            SimpleRegression sr = new SimpleRegression(true);
            for (Point2D point : row) {
                sr.addData(point.getX(), point.getY());
            }

            // Csak akkor tároljuk az egyenest, ha a meredeksége adott értékek között mozog.
            if (er > Math.abs(sr.getSlope())) {
                Line2D l = new Line2D.Double(0, sr.predict(0), imgp.getBufferedImage().getWidth(), sr.predict(imgp.getBufferedImage().getWidth()));
                rowLines.add(l);
            }
        }

        // Sorokra illeszkedő egyenesek rendezése.
        Collections.sort(rowLines, new Comparator<Line2D>() {
            @Override
            public int compare(Line2D t, Line2D t1) {
                return (int) (t.getY1() - t1.getY1());
            }
        });

        // Oszlopokra illeszkedő egyenesek előállítása.
        LinkedList<Line2D> colLines = new LinkedList<>();
        for (LinkedList<Point2D> col : cols) {
            SimpleRegression sr = new SimpleRegression(true);
            for (Point2D point : col) {
                sr.addData(point.getY(), point.getX());
            }

            if (er > Math.abs(sr.getSlope())) {
                Line2D l = new Line2D.Double(sr.predict(0), 0, sr.predict(imgp.getBufferedImage().getHeight()), imgp.getBufferedImage().getHeight());
                colLines.add(l);
            }
        }

        // Oszlopokra illeszkedő egyenesek rendezése.
        Collections.sort(colLines, new Comparator<Line2D>() {
            @Override
            public int compare(Line2D t, Line2D t1) {
                return (int) (t.getX1() - t1.getX1());
            }
        });

        // Rácspontok meghatározása.
        Point2D gridPoints[][] = new Point2D[rowLines.size()][colLines.size()];
        for (int r = 0; r < rowLines.size(); r++) {
            for (int c = 0; c < colLines.size(); c++) {
                gridPoints[r][c] = LineIntersection.getLineLineIntersection(rowLines.get(r), colLines.get(c));
            }
        }

        HashMap<String, LinkedList<Line2D>> lines = new HashMap<>();
        lines.put("rows", rowLines);
        lines.put("cols", colLines);

        return new HoleFinderResult(
                image,
                imgp.getBufferedImage(),
                points,
                gridPoints,
                lines);
    }
}
